package org.xx.armory.spring5.mvc;

import org.aopalliance.intercept.MethodInterceptor;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMethodMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.security.Principal;
import java.util.Date;

import static org.apache.commons.lang3.time.DateFormatUtils.format;
import static org.apache.commons.lang3.time.DateFormatUtils.formatUTC;
import static org.apache.commons.text.StringEscapeUtils.escapeJson;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.xx.armory.commons.TextUtils.ellipse;
import static org.xx.armory.spring5.mvc.TaskLogStage.FAIL;
import static org.xx.armory.spring5.mvc.TaskLogStage.START;
import static org.xx.armory.spring5.mvc.TaskLogStage.SUCCEED;

@Configuration
public class TaskLogConfig {
    public static final String TASK_MARKER = "TASK";
    private static final Marker TASK_MARKER_ = MarkerFactory.getMarker(TASK_MARKER);
    private static final String TASK_LOG_FORMAT = "{\"timestamp\":\"{}\",\"stage\":\"{}\",\"category\":\"{}\",\"action\":\"{}\",\"user\":\"{}\",\"detail\":{}}";

    /**
     * ISO8601格式。
     * 比如: {@literal 2007-04-05T14:30Z}。
     *
     * @see <a href="https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations">ISO8601 Wiki</a>
     */
    private static final String ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssX";

    private static final char DOUBLE_QUOTE = '"';

    private String quote(
            String src
    ) {
        return DOUBLE_QUOTE + src + DOUBLE_QUOTE;
    }

    private void appendKeyValue(
            StringBuilder builder,
            String key,
            Object value
    ) {
        if (key == null || key.isBlank()) {
            return;
        }
        if (value == null) {
            return;
        }

        final var s = escapeJson(value.toString());
        if (value instanceof CharSequence) {
            if (s.isEmpty()) {
                return;
            }
            value = quote(s);
        } else {
            value = s;
        }

        key = quote(escapeJson(key));
        builder.append(key).append(':').append(value);
    }

    @Bean
    public MethodInterceptor taskLogInterceptor() {
        return invocation -> {
            final var outputTaskLogAnnotation = findAnnotation(invocation.getMethod(), OutputTaskLog.class);
            if (outputTaskLogAnnotation != null) {
                final var detailBuilder = new StringBuilder("{");
                final var invokedParameters = invocation.getMethod().getParameters();
                final var invokedParameterValues = invocation.getArguments();

                for (var i = 0; i < invokedParameters.length; ++i) {
                    if (detailBuilder.length() > 1) {
                        detailBuilder.append(",");
                    }

                    final var param = invokedParameters[i];
                    if (Principal.class.isAssignableFrom(param.getType())) {
                        continue;
                    }

                    final var key = param.getName();
                    final var value = invokedParameterValues[i];

                    if (value == null) {
                        appendKeyValue(detailBuilder, key, null);
                    } else if (value instanceof BigDecimal) {
                        appendKeyValue(detailBuilder, key, ((BigDecimal) value).toPlainString());
                    } else if (value instanceof Number) {
                        appendKeyValue(detailBuilder, key, value);
                    } else if (value instanceof Date) {
                        appendKeyValue(detailBuilder, key, format((Date) value, ISO8601_FORMAT));
                    } else if (value instanceof byte[]) {
                        appendKeyValue(detailBuilder, key, "byte[" + Array.getLength(value) + "]");
                    } else if (value.getClass().isArray()) {
                        appendKeyValue(detailBuilder, key, value.getClass().getComponentType().getTypeName() + "[" + Array.getLength(value) + "]");
                    } else if (value instanceof CharSequence) {
                        appendKeyValue(detailBuilder, key, ellipse((CharSequence) value, 1024));
                    } else {
                        appendKeyValue(detailBuilder, key, value.toString());
                    }
                }
                detailBuilder.append("}");

                final var now = new Date();
                final var category = escapeJson(outputTaskLogAnnotation.category());
                final var action = escapeJson(outputTaskLogAnnotation.action());
                final var authentication = SecurityContextHolder.getContext().getAuthentication();
                final var username = authentication != null && authentication.getName() != null ? escapeJson(authentication.getName()) : "";
                final var detail = detailBuilder.toString();
                final var logger = LoggerFactory.getLogger("org.xx.armory.TaskLog");

                logger.info(TASK_MARKER_, TASK_LOG_FORMAT,
                            formatUTC(now, ISO8601_FORMAT), START, category, action, username, detail);

                final Object result;
                try {
                    result = invocation.proceed();

                    logger.info(TASK_MARKER_, TASK_LOG_FORMAT,
                                formatUTC(now, ISO8601_FORMAT), SUCCEED, category, action, username, detail);
                } catch (Exception ex) {
                    logger.info(TASK_MARKER_, TASK_LOG_FORMAT,
                                formatUTC(now, ISO8601_FORMAT), FAIL, category, action, username, detail);
                    throw ex;
                }

                return result;
            } else {
                return invocation.proceed();
            }
        };
    }

    @Bean
    public PointcutAdvisor taskLogAdvisor(
            @Autowired @Qualifier("taskLogInterceptor") MethodInterceptor taskLogInterceptor
    ) {
        final var advisor = new DefaultBeanFactoryPointcutAdvisor();
        advisor.setPointcut(new Pointcut() {
            @Override
            public ClassFilter getClassFilter() {
                return clazz -> clazz.getTypeName().endsWith("Controller");
            }

            @Override
            public MethodMatcher getMethodMatcher() {
                return new AnnotationMethodMatcher(OutputTaskLog.class, false);
            }
        });
        advisor.setAdvice(taskLogInterceptor);
        return advisor;
    }
}
